package net.zoneland.zrdp.framework.config;

import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

import com.branchitech.utils.SystemHelper;

import net.zoneland.zrdp.common.exception.ServiceException;


/**
 * Mybatis支持*匹配扫描包
 *
 * @author zonevue
 */
@Configuration(proxyBeanMethods = false)
@ConfigurationProperties(prefix = "mybatis")
public class MyBatisConfig {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyBatisConfig.class);

    private String typeAliasesPackage;

    private String mapperLocations;

    private String configLocation;

    private String tablePrefix;

    private CustomMyBatisConfig configuration = new CustomMyBatisConfig();

    private String sysdateFn;

    private String ifnullFn;

    private String escapeChar;

    private String limitOne;

    private String deptFindInSet;
    private String deptFindNotInSet;

    /**
     * comment字段兼容处理, oracle此字段为关键词.
     */

    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    private static String computeTypeAliasesPackage(final String typeAliasesPackage) {
        final ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);

        final Set<String> allResult = Arrays.stream(StringUtils.split(typeAliasesPackage, ","))
            .map(String::trim)
            .flatMap(packageName -> scanPackageForResources(packageName, resolver, metadataReaderFactory))
            .collect(Collectors.toSet());

        if (allResult.isEmpty()) {
            throw new ServiceException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
        }

        return String.join(",", allResult);
    }

    private static Stream<String> scanPackageForResources(final String packageName, final ResourcePatternResolver resolver,
                                                       final MetadataReaderFactory metadataReaderFactory) {
        final String locationPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
            + ClassUtils.convertClassNameToResourcePath(packageName) + "/" + DEFAULT_RESOURCE_PATTERN;

        try {
            return Arrays.stream(resolver.getResources(locationPattern))
                .filter(Resource::isReadable)
                .map(resource -> getPackageNameFromResource(resource, metadataReaderFactory))
                .filter(Optional::isPresent)
                .map(Optional::get);
        } catch (final IOException e) {
            LOGGER.error("出现IO错误：{}", packageName, e);
            return Stream.empty();
        }
    }

    private static Optional<String> getPackageNameFromResource(final Resource resource, final MetadataReaderFactory metadataReaderFactory) {
        try {
            final MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
            final String className = metadataReader.getClassMetadata().getClassName();
            return Optional.of(Class.forName(className).getPackage().getName());
        } catch (final ClassNotFoundException | IOException e) {
            LOGGER.error("出现错误：{}", resource.getDescription(), e);
            return Optional.empty();
        }
    }


    public Resource[] resolveMapperLocations(final String[] mapperLocations) {

        if (mapperLocations == null) {
            return new Resource[0];
        }

        final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        return Arrays.stream(mapperLocations)
            .flatMap(mapperLocation -> {
                try {
                    return Arrays.stream(resourceResolver.getResources(mapperLocation));
                } catch (IOException e) {
                    LOGGER.error("出现IO错误：{}", mapperLocation, e);
                    return Stream.empty();
                }
            })
            .toArray(Resource[]::new);
    }


    @Bean
    public SqlSessionFactory sqlSessionFactory(final DataSource dataSource) throws Exception {

        final String newTypeAliasesPackage = computeTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setSqlSessionFactoryBuilder(new SqlSessionFactoryBuilder() {

            @Override
            public SqlSessionFactory build(org.apache.ibatis.session.Configuration config) {
                config.setUseGeneratedKeys(config.isUseGeneratedKeys());
                return super.build(config);
            }

        });
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(newTypeAliasesPackage);
        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
        sessionFactory.setConfiguration(configuration);
        final Properties configurationProperties = new Properties();
        if (StringUtils.isNotEmpty(tablePrefix)) {
            // 设置mybatis全局变量
            configurationProperties.put("tablePrefix", tablePrefix);
        }
        configurationProperties.put("sysdateFn", StringUtils.isEmpty(sysdateFn) ? "sysdate()" : sysdateFn);
        configurationProperties.put("ifnullFn", StringUtils.isEmpty(ifnullFn) ? "ifnull" : ifnullFn);
        configurationProperties.put("limitOne", StringUtils.isEmpty(limitOne) ? "limit 1" : limitOne);
        configurationProperties.put("deptFindInSet", StringUtils.isEmpty(deptFindInSet)
            ? "find_in_set(#{deptId}, ancestors)" : deptFindInSet);
        configurationProperties.put("deptFindNotInSet", StringUtils.isEmpty(deptFindNotInSet)
            ? "!find_in_set(#{deptId}, ancestors)" : deptFindNotInSet);
        configurationProperties.put("escapeChar", escapeChar == null || escapeChar.length() > 1 ? "" : escapeChar);
       sessionFactory.setConfigurationProperties(configurationProperties);
        return sessionFactory.getObject();
    }

    /**
     * @return the sysdateFn
     */
    public String getSysdateFn() {
        return sysdateFn;
    }

    /**
     * @param sysdateFn the sysdateFn to set
     */
    public void setSysdateFn(final String sysdateFn) {
        this.sysdateFn = sysdateFn;
    }

    /**
     * @return the typeAliasesPackage
     */
    public String getTypeAliasesPackage() {
        return typeAliasesPackage;
    }

    /**
     * @return the mapperLocations
     */
    public String getMapperLocations() {
        return mapperLocations;
    }

    /**
     * @return the configLocation
     */
    public String getConfigLocation() {
        return configLocation;
    }

    /**
     * @return the tablePrefix
     */
    public String getTablePrefix() {
        return tablePrefix;
    }

    /**
     * @param mapperLocations the mapperLocations to set
     */
    public void setMapperLocations(final String mapperLocations) {
        this.mapperLocations = mapperLocations;
    }

    /**
     * @param configLocation the configLocation to set
     */
    public void setConfigLocation(final String configLocation) {
        this.configLocation = configLocation;
    }

    /**
     * @param tablePrefix the tablePrefix to set
     */
    public void setTablePrefix(final String tablePrefix) {
        this.tablePrefix = tablePrefix;
    }

    /**
     * @param typeAliasesPackage the typeAliasesPackage to set
     */
    public void setTypeAliasesPackage(final String typeAliasesPackage) {
        this.typeAliasesPackage = typeAliasesPackage;
    }

    /**
     * @return the ifnullFn
     */
    public String getIfnullFn() {
        return ifnullFn;
    }

    /**
     * @param ifnullFn the ifnullFn to set
     */
    public void setIfnullFn(final String ifnullFn) {
        this.ifnullFn = ifnullFn;
    }

    /**
     * @return the configuration
     */
    public CustomMyBatisConfig getConfiguration() {
        return configuration;
    }

    /**
     * @param configuration the configuration to set
     */
    public void setConfiguration(final CustomMyBatisConfig configuration) {
        this.configuration = configuration;
    }

    /**
     * @return the limitOne
     */
    public String getLimitOne() {
        return limitOne;
    }

    /**
     * @param limitOne the limitOne to set
     */
    public void setLimitOne(final String limitOne) {
        this.limitOne = limitOne;
    }

    /**
     * @return the deptFindInSet
     */
    public String getDeptFindInSet() {
        return deptFindInSet;
    }

    /**
     * @param deptFindInSet the deptFindInSet to set
     */
    public void setDeptFindInSet(final String deptFindInSet) {
        this.deptFindInSet = deptFindInSet;
    }


    /**
     * @return the escapeChar
     */
    public String getEscapeChar() {
        return escapeChar;
    }

    /**
     * @param escapeChar the escapeChar to set
     */
    public void setEscapeChar(final String escapeChar) {
        this.escapeChar = escapeChar;
    }

    /**
     * @return the deptFindNotInSet
     */
    public String getDeptFindNotInSet() {
        return deptFindNotInSet;
    }

    /**
     * @param deptFindNotInSet the deptFindNotInSet to set
     */
    public void setDeptFindNotInSet(final String deptFindNotInSet) {
        this.deptFindNotInSet = deptFindNotInSet;
    }

    public static class CustomMyBatisConfig extends org.apache.ibatis.session.Configuration {

        //当此值为true进强制采用NoKeyGenerator 适用于oracle
        private KeyGenerator forceKeyGenerator;

        @Override
        public KeyGenerator getKeyGenerator(final String id) {
            if (forceKeyGenerator != null) {
                return forceKeyGenerator;
            }
            return super.getKeyGenerator(id);
        }

        @Override
        public boolean hasKeyGenerator(final String id) {
            if (forceKeyGenerator != null) {
                return true;
            }
            return super.hasKeyGenerator(id);
        }


        /**
         * @param forceKeyGenerator the forceNoKeyGenerator to set
         */
        public void setForceKeyGenerator(final String forceKeyGenerator) {
            if ("NoKeyGenerator".equals(forceKeyGenerator)) {
                this.forceKeyGenerator = NoKeyGenerator.INSTANCE;
            } else if ("Jdbc3KeyGenerator".equals(forceKeyGenerator)) {
                this.forceKeyGenerator = Jdbc3KeyGenerator.INSTANCE;
            } else if (StringUtils.isNotEmpty(forceKeyGenerator)){
                this.forceKeyGenerator = SystemHelper.createInstance(forceKeyGenerator, null);
            }
        }
    }

}
